JVM

JVM 垃圾收集器-内存分配

Posted by 余腾 on 2019-04-08
Estimated Reading Time 41 Minutes
Words 10.4k In Total
Viewed Times

一、垃圾收集器与内存分配策略

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,
墙外面的人想进去,墙里面的人却想出来。

JVM 体系结构

由于 JVM 跨平台性的设计,Java的指令都是根据栈来设计的。
不同平台CPU架构不同,所以不能设计为基于寄存器的。

  • 基于栈式架构
    • 优点:跨平台, 指令集小,指令多,编译器容易实现。
    • 缺点:性能下降比寄存器差,实现同样的功能需要更多的指令。

GC 作用于 方法区、堆

元空间(Java8)与永久代Java7)之间最大的区别在于:
永久代使用的 JVM 的堆内存, 但是 Java8 以后的元空间并不在虚拟机中而是使用本机物理内存。

H8C~3ZLH0B@[I_L6]VD8G{E.png

PC 寄存器

Register 的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行。这里的 PC 寄存器 并非是广义上所指的物理寄存器,JVM 中的 PC 寄存器是对物理 PC 寄存器的一种抽象模拟,它是一块很小的内存空间,几乎可以忽略不计,也是运行速度最快的存储区域。每条线程都有它自己的 PC Register,线程私有,生命周期与线程的生命周期保持一致。

作用:用来存储指向下一条指令的地址,也是即将要执行的指令代码,由执行引擎读取下一条指令。

虚拟机栈

每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧 Stack Frame,对应 Java 方法的调用。线程私有,生命周期与线程的生命周期保持一致。

作用:主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

栈帧

  • 局部变量表 Local Variables
  • 操作数栈 Operand Stack
  • 动态链接 Dynamic Linking
  • 方法返回地址 Return Address
  • 附加信息


30_8695454HA{E}8VUPYI]G.png

1、引入计数法

概念:

  • 在对象中引入计数器(无符号整数),用于记录有多少对象引用了该对象。
  • 每当有一个地方引用它时,计数器加1;引用失效时,计数器减1。任何计数器为0的对象都是不可能再被使用的。

优点:
1、即刻回收垃圾,在更改引用时就知道该对象是否为垃圾若是垃圾立马进行回收。
2、STW短,回收垃圾不需要遍历堆了。(Stop-The-World)
3、不需要根据 GC Roots 遍历。

缺点:
1、计数器值增减频繁。
2、计数器需要占用很多位。
3、实现繁琐,更新引用时很容易导致内存泄露。
4、循环引用无法回收(最重要的缺点)。

引用计数算法的缺陷

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ReferenceCountingGC {
public Object instance = null;
private static int int_1MB = 1024 * 1024;

private byte[] bigSize = new byte[10 * int_1MB];

public static void testGC() {
ReferenceCountingGC a = new ReferenceCountingGC();
ReferenceCountingGC b = new ReferenceCountingGC();
a.instance = b;
b.instance = a;

a = null;
b = null;
System.gc();
}

public static void main(String[] args) {
testGC();
}
}

代码中的testGC()方法:

  • 对象a和b都有字段instance,赋值令a.instance = b 及b.instance = a,除此之外,这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

2、可达性分析算法 GC Roots

在Java中,是通过可达性分析(Reachability Analysis)来判定对象是否存活的。

内存中已经不再被使用到的空间就是垃圾。

概念

  • 该算法的基本思路就是通过一系列被称为引用链(GC Roots)的对象(枚举根节点)作为起点,从这些节点开始向下搜索,搜索走过的路径被称为引用链(Reference Chain)
  • 当一个对象到GC Roots没有任何引用链相连时(即从GC Roots节点到该节点不可达),则证明该对象是不可用的。

如图所示:

  • object1~object4 对GC Roots 都是可达的,说明不可被回收。
  • object5 和 object6、object7对 GC Roots节点 不可达,说明其可以被回收。

在Java中,可作为GC Roots的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

3、引用

无论通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,都是判断对象是否存活都与”引用“有关。

在JDK1.2之后,Java对引用的概念做了扩充,将引用分为四类(这四种引用的强度依次递减):

  • 强引用(Strong Reference)
  • 软引用(Soft Reference)
  • 弱引用(Weak Reference)
  • 虚引用(Phantom Reference)
HP4_XUP_U8IPRW()YFBQY~S.png

强引用(Strong Reference)

  • 强引用就是指在程序代码中普遍存在的,类似 Object obj = new Object() 这类的引用。只要强引用在,垃圾收集器永远不会收集被引用的对象。也就是说,宁愿出现内存溢出OOM,也不会回收这些对象,OOM发生的主要原因。
1
2
3
4
5
6
7
8
9
10
public class StrongReference {
public static void main(String[] args) {
Object obj1 = new Object();//这样定义的默认就是强引用
Object obj2 = obj1;//obj2 5引用赋值
obj1 = null;//置空
System.gc();
System.out.println(obj1);//TODO null
System.out.println(obj2);//TODO java.lang.Object@74a14482
}
}

软引用

  • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  • 场景 加载大量图片,可以使用软引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.lang.ref.SoftReference;

public class SoftReferenceDemo {

/*
*内存够用的时候就保留,不够用就回收!
*/
public static void softRef_Memory_Enough() {
Object object1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(object1);
System.out.println(object1);//TODO java.lang.Object@74a14482
System.out.println(softReference.get());//TODO java.lang.Object@74a14482
object1 = null;
System.gc();
System.out.println(object1);//TODO null
System.out.println(softReference.get());//TODO java.lang.Object@74a14482
}

/**
* JVM配置,故意产生大对象并配置小的内存,让它内存不够用了 导致00M,看软引用的回收情况
* -Xms5m -Xmx5m -XX: +PrintGCDetails
*/
public static void softRef_Memory_NotEnough() {
Object object1 = new Object();
SoftReference<Object> softReference = new SoftReference<>(object1);
System.out.println(object1);//TODO java.lang.Object@1540e19d
System.out.println(softReference.get());//TODO java.lang.Object@1540e19d

object1 = null;

try {
byte[] bytes = new byte[30 * 1024 * 1014];
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println(object1);//TODO null
System.out.println(softReference.get());//TODO null
}
}

public static void main(String[] args) {
// softRef_Memory_Enough();
softRef_Memory_NotEnough();
}
}

弱引用

  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 场景 加载大量图片,可以使用弱引用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.ref.WeakReference;

public class WeakReferenceDemo {
public static void main(String[] args) {

Object object1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(object1);
System.out.println(object1);//TODO java.lang.Object@74a14482
System.out.println(weakReference.get());//TODO java.lang.Object@74a14482

object1 = null;
System.gc();

System.out.println(object1);//TODO null
System.out.println(weakReference.get());//TODO null
}
}

WeakHashMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.HashMap;
import java.util.WeakHashMap;

public class WeakHashMapDemo {

public static void main(String[] args) {
myHashMap();
System.out.println("----------------");
myWeakHashMap();
}

private static void myWeakHashMap() {
WeakHashMap<Integer, String> weakHashMap = new WeakHashMap<>();
Integer key = new Integer(2);
String value = "WeakHashMap";
weakHashMap.put(key, value);
System.out.println(weakHashMap);//{2=WeakHashMap}

key = null;
System.out.println(weakHashMap);//{2=WeakHashMap}

System.gc();
System.out.println(weakHashMap);//TODO {}
}

private static void myHashMap() {

HashMap<Integer, String> map = new HashMap<>();
Integer key = new Integer(1);
String value = "HashMap";
map.put(key, value);
System.out.println(map);//{1=HashMap}

key = null;
System.out.println(map);//{1=HashMap}

System.gc();
System.out.println(map);//{1=HashMap}
}
}

虚引用

  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,也不能单独使用,必须和引用队列(ReferenceQueue)联合使用。在任何时候都有可能被垃圾回收器回收。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
  • 创建引用的时候可以指定关联的队列,当GC 释放对象内存的时候,会将引用加入到引用队列,如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式, JVM允许我们在对象被销毁后,做一些我们自己想做的事情。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class PhantomReferenceDemo {

public static void main(String[] args) throws InterruptedException {

Object object1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(object1, referenceQueue);

System.out.println(object1);//java.lang.Object@74a14482
System.out.println(phantomReference.get());//TODO null
System.out.println(referenceQueue.poll());//TODO null

System.out.println("=========GC 后放入引用队列===========");

object1 = null;
System.gc();
Thread.sleep(500);

System.out.println(object1);//TODO null
System.out.println(phantomReference.get());//TODO null
System.out.println(referenceQueue.poll());//TODO java.lang.ref.PhantomReference@1540e19d
}
}
/*
java.lang.Object@74a14482
null
null
=========GC 后放入引用队列===========
null
null
java.lang.ref.PhantomReference@1540e19d
*/

引用队列 ReferenceQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;

public class ReferenceQueueDemo {

public static void main(String[] args) throws InterruptedException {

Object object1 = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
WeakReference<Object> weakReference = new WeakReference<>(object1, referenceQueue);

System.out.println(object1);
System.out.println(weakReference.get());
System.out.println(referenceQueue.poll());

System.out.println("=========GC后 对象引用被放入引用队列========");

object1 = null;
System.gc();
Thread.sleep(500);
System.out.println(object1);
System.out.println(weakReference.get());
System.out.println(referenceQueue.poll());
}
}

//TODO
java.lang.Object@74a14482
java.lang.Object@74a14482
null
=========GC后 对象引用被放入引用队列========
null
null
java.lang.ref.WeakReference@1540e19d

生存还是死亡

要真正宣告一个对象死亡,至少要经历两次标记过程:

  • 如果对象在进行可达性分析后发现没有与 GC Rooots 相连接的引用链,那它将会被第一次标记并且进行一次筛选。

    • 筛选的条件是此对象是否有必要执行 finalize() 方法: 当对象没有覆盖finalize() 方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
  • 如果对象要在finalize()中成功拯救自己,只要重新与引用链上的任何一个对象建立关联即可。那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

二、垃圾收集算法

1、标记 — 清除算法(Mark-Sweep)

标记 - 清除算法是最基础的收集算法,算法分为两个阶段“标记“和”清除。
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 用于老年代

缺点:

  • 效率问题:标记和清除两个过程的效率都不高;
  • 空间问题:标记清除之后会产生大量不连续的内存碎片。
  • 空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

2、复制算法(Copying)

实现简单,运行高效。适合年轻代

  • 为了解决效率:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
    当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

缺点:

  • 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
  • 空间问题:內存缩小了一半;需要額外空间做分配担保(老年代)。

3、标记 — 整理算法(Mark-Compact)

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法

标记过程与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,
而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。


4、分代收集算法(Generational Collection)

GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。

“分代收集”算法:把 Java堆 分为 新生代 和 老年代,根据各个年代的特点采用适当的收集算法。

在新生代中: 每次垃圾收集时都有大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

在老年代中: 因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。


三、垃圾收集器

Java8 默认垃圾收集器 -XX:+UseParallelGC (并行GC)

1
2
3
4
5
6
7
8
9
java -XX:+PrintCommandLineFlags -version

-XX:InitialHeapSize=132510208
-XX:MaxHeapSize=2120163328
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation
-XX:+UseParallelGC

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

  • Serial:串行
  • Parallel:并行
  • CMS(Concurrent Mark Sweep):并发标记
  • GC:G1(Garbage First)

HotSpot虚拟机:包含的所有收集器(⑦大垃圾收集器)如下图所示:

FBHD2J2GFYB`89K}NR$IK[Q.png

相关概念

并行(Parallel): 指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行的可能会交替执行)
吞吐量(Throughput): 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。

  • 吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
  • 假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

Server模式/Client模式 操作系统:

  • 32位 Window 操作系统, ,不论硬件如何都默认使用 Client 的JVM模式
  • 32位其它操作 系统,2G内存同时有2个cpu以上用 Server 模式,低于该配置还是 Client 模式
  • 64位 only server 模式
简写 意义
DefNew Default New Generation - Serial
Tenured Old - Serial Old
ParNew Parallel New Generation
PSYoungGen Parallel Scavenge
ParOldGen Parallel Old Generation

Minor GC 和 Major/Full GC

新生代GC(Minor GC):

  • 指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

老年代GC(Major GC / Full GC):

  • 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

新生代收集器

1、Serial 收集器 (Client 模式)

它是一个单线程收集器,简单而高效。

Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法新生代收集器

它在进行垃圾收集时,必须暂停其他所有的工作线程,直至 Serial 收集器 收集结束为止(STW)

  • 这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作线程全部停掉,这对很多应用来说是难以接收的

Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

当新生代激活了 Serial 收集器,老年代自动激活 Serial Old 收集器。

1
-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC

下图展示了Serial /Serial Old收集器(老年代采用Serial Old收集器)的运行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.ArrayList;
import java.util.List;

/**
* @author Yu
* java -XX:+PrintCommandLineFlags -version
* <p>
* -XX:InitialHeapSize=132510208
* -XX:MaxHeapSize=2120163328
* -XX:+PrintCommandLineFlags
* -XX:+UseCompressedClassPointers
* -XX:+UseCompressedOops
* -XX:-UseLargePagesIndividualAllocation
* -XX:+UseParallelGC
*
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialGC
* GC (Allocation Failure) [DefNew Default New Generation
* Full GC (Allocation Failure) [Tenured Old
*
* //TODO DefNew + Tenured
*/
public class SerialGC {
public static void main(String[] args) throws InterruptedException {

int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

2、ParNew 收集器 (Server 模式)

ParNew 收集器 就是 Serial 收集器 的多线程版本,它也是一个新生代收集器
除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。

ParNew 收集器 除了使用多线程收集外,其他与 Serial收集器 相比并无太多创新之处,但它却是许多运行在 Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.ArrayList;
import java.util.List;

/**
* java -XX:+PrintCommandLineFlags -version
*
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParNewGC
*
* [GC (Allocation Failure) [ParNew Parallel New Generation
* [Full GC (Allocation Failure) [Tenured Old
*
* //TODO ParNew + Tenured
*
* Java HotSpot(TM) 64-Bit Server VM warning:
* Using the ParNew young collector with the Serial old collector is deprecated and will likely be removed in a future release
* 不推荐将 ParNew收集器 与 Serial Old收集器 一起使用,并且可能会在将来的版本中删除
* 推荐 ParNew收集器 与 CMS收集器一起使用
*/
public class ParNewGC {
public static void main(String[] args) throws InterruptedException {

int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

3、Parallel Scavenge 收集器

Java8 新生代默认收集器,新生代老年代都用并行收集器(Parallel Scavenge + Parallel Old)。

Parallel Scavenge 收集器 也是一个并行的多线程新生代收集器,它也使用复制算法
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同:

  • CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间;
  • Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)

停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。自适应调解策略

  • 另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用;
  • 所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用,现在与 Parallel Old 配合使用。

-XX:+UseParallelGC 或 -XX:+UseParallelOldGC(可互相激活)使用Parallel Scanvenge收集器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.ArrayList;
import java.util.List;

/**
* @author Yu
*
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelGC
*
* [GC (Allocation Failure) [PSYoungGen: Parallel Scavenge
* [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: Parallel Old Generation
*
* //TODO PSYoungGen + ParOldGen
*/
public class ParallelGC {
public static void main(String[] args) throws InterruptedException {

int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

老年代收集器

1、Serial Old 收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。

此收集器的意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:

  • 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author Yu
*
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseSerialOldGC
* //TODO 此参数Java8已经被优化,不存在了
* Error: Could not create the Java Virtual Machine.
* Error: A fatal exception has occurred. Program will exit.
* Unrecognized VM option 'UseSerialOldGC'
* Did you mean '(+/-)UseSerialGC'?
*
* 错误:无法创建Java虚拟机。
* 错误:发生了致命异常。 程序将会退出。
* 无法识别的VM选项'UseSerialOldGC'
* 你的意思是'(+/-)UseSerialGC'?
*/
public class SerialOldGC {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

2、Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程“标记-整理”算法。

前面已经提到过,这个收集器是在 JDK 1.6 中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择,所以在Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

Parallel Old收集器的工作流程与Parallel Scavenge相同。

这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;
import java.util.List;

/**
* @author Yu
*
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseParallelOldGC
*
* [GC (Allocation Failure) [PSYoungGen: Parallel Scavenge
* [Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: Parallel Old Generation
*
* //TODO PSYoungGen + ParOldGen
*/
public class ParallelOldGC {
public static void main(String[] args) throws InterruptedException {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

3、CMS 收集器

CMS(Concurrent Mark Sweep)并发标记收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用这些应用都非常重视服务的响应速度。
从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。清除 GC Roots 不可达对象

CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark): 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark): 进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark): 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,
所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。

老年代使用CMS -XX:+UseConcMarkSweepGC,新生代自动激活使用 ParNewGC 收集器。并且和 Serial Old收集器组合使用,Serial Old将作为CMS出错的后备收集器。

通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间:

优点:
CMS是一款优秀的收集器,并发收集低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。


缺点:

  • 对CPU资源非常敏感。
  • CMS 必须要在老年代堆内存用尽之前完成垃圾回收,否则 CMS 回收失败时,将触发保护机制,串行老年代收集器将会以 STW 的方式进行一次 GC,从而造成较大的停顿时间。
  • 无法处理浮动垃圾(Floating Garbage),由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。
  • 标记-清除算法导致的空间碎片,这意味着收集结束时会有大量空间碎片产生。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.ArrayList;
import java.util.List;

/**
* @author Yu
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+PrintCommandLineFlags -XX:+UseConcMarkSweepGC
* 老年代使用CMS -XX:+UseConcMarkSweepGC,新生代自动激活使用 ParNewGC 收集器。
*
* [GC (Allocation Failure) [ParNew:
* [Full GC (Allocation Failure) [CMS:
*
* //TODO ParNew + CMS
* [GC (CMS Initial Mark)
* [CMS-concurrent-mark-start]
* [GC (CMS Final Remark)
* [CMS-concurrent-sweep-start]
*/
public class CMSGC {
public static void main(String[] args) {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

垃圾收集器的选择

组合:

  • 单CPU或小内存,单机程序。
    • -XX:+UseSerialGC
  • 多CPU,需要最大吞吐量,如后台计算型应用。
    • -XX:+UseParallelGC/-XX:+UseParallelOldGC
  • 多CPU ,追求低停顿时间,需快速响应如互联网应用。
    • -XX:+UseConcMarkSweepGC/-XX:+UseParNewGC

四、G1收集器

G1(Garbage-First)收集器是收集器技术发展最前沿的成果之一,面向服务端应用的垃圾收集器。

Java9 默认收集器

与其他GC收集器相比,G1具备如下特点:

  • 并行与并发: G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  • 分代收集: 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果,G1没有内存碎片。

  • 空间整合: G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。

  • 可预测的停顿: 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。


1、横跨整个堆内存

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region 1M~32M不等,必须是2的幂,默认为2048个分区)虽然还保留新生代和老年代的概念,新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续,不要求对象存储一定是物理上的连续,只要逻辑上连续即可)的集合。


2、建立可预测的时间模型

有计划地避免在整个Java堆中进行全区域的垃圾收集。

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。


3、避免全堆扫描——Remembered Set

G1把Java堆分为多个Region,就是“化整为零”。

但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set
虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象)。如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

4、G1收集器收集步骤

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking): 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。

  • 并发标记(Concurrent Marking): 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。

  • 最终标记(Final Marking): 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。

  • 筛选回收(Live Data Counting and Evacuation): 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。


通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段(Safepoint处):

Java11、12 -> ZGC


五、总结 收集器各个功能

收集器 串/并行/并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 Both 标整+复制算法 响应速度优先 面向服务端应用,将来替换CMS

垃圾收集的相关常用参数


参数 描述
UseSerialGC 虚拟机运行在Client模式下的默认值,打开此开关后,使用Serial +Serial Old的收集器组合进行内存回收。
UseParNewGC 打开此开关后,使用ParNew + Serial Old的收集器组合进行内存回收
UseConcMarkSweepGC 打开此开关后,使用ParNew + CMS + Serial Old的收集器组合进行内存回收。SerialOld收集器将作为CMS收集器出现ConcurrentModeFailure失败后的后备收集器使用
UseParallelGC 虚拟机运行在Server模式下的默认值,打开此开关后,使用ParallelScavenge + Serial Old (PS MarkSweep)的收集器组合进行内存回收
UseParallelOldGC 打开此开关后,使用Parallel Scavenge + Parallel Old的收集器组合进行内存回收
SurvivorRatio 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden : Survivor-8: 1
PretenureSizeThreshold 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
MaxTenuringThreshold 晋升到老年代的对象年龄。每个对象在坚持过- - 次Minor GC之后,年龄就增加1,当超过这个参数值时就进人老年代
UseAdaptiveSizePolicy 动态调整Java堆中各个区域的大小以及进入老年代的年龄
HandlePromotionFailure 是否允许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的所有对象都存活的极端情况
ParallelGCThreads 设置并行GC时进行内存回收的线程数

六、JVM参数类型

  • 标配参数

    • -version
    • -help
    • java -showversion
  • X参数

    • -Xint:解释执行
    • -Xcomp:第一次使用就编译成本地代码
    • -Xmixed:混合模式
  • XX参数

    jps -l、jinfo -flags、jinfo -flag PrintGCDetails xxxx,查看当前运行程序的配置。

    • Boolean类型
      • 公式:-XX:+ (表示开启)或者-(表示关闭)某个属性值;
      • Case:jps -l、jinfo -flag PrintGCDetails xxxx
    • KV设值类型
      • 公式:-XX:属性key=属性值value
      • Case:-XX:MetaspaceSize=128m
      • -Xms、-Xmx
        • -Xms:等价于 -XX:InitialHeapSize
        • -Xmx:等价于 -XX:MaxHeapSize

JVM调优相关

  • java -XX: +PrintFlagsInitial:查看默认参数
  • java -XX:+ PrintFlagsFinal -version:查看修改更新过的参数
  • = / :=
    • = 默认参数
    • := 修改过的参数
  • -XX:+PrintCommandLineFlags -version
1
2
3
4
5
6
7
8
public class JVM {
public static void main(String[] args) {
long totalMemory = Runtime.getRuntime().totalMemory();//返回 Java虚拟机中的内存总量。 1/64
long maxMemory = Runtime.getRuntime().maxMemory();//返回 Java虚拟机试图使用的最大内存量。1/4
System.out.println("TOTAL_MEMORY(-Xms) =" + totalMemory + " (字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");
System.out.println("MAX_MEMORY(-Xmx) =" + maxMemory + " (字节)、" + (maxMemory / (double) 1024 / 1024) + " MB");
}
}

常见参数

  • -Xms:初始大小内存、默认物理内存为 1/64。等价于 -XX:InitialHeapSize
  • -Xmx:最大分配内存、默认物理内存为 1/4。等价于 -XX:MaxHeapSize
  • -Xss:设置单个线程栈的大小,一般默认为512k~1024K。等价于 -XX:ThreadStackSize
  • -Xmn:设置年轻代大小。
  • -XX:MetaspaceSize
    • 元空间(Java8)与永久代Java7)之间最大的区别在于:
    • 永久代使用的JVM的堆内存, 但是java8以后的元空间并不在虚拟机中而是使用本机物理内存。
  • -XX:+PrintGCDetails 输出详细GC收集日志信息
  • -XX:SurvivorRatio
    • 新生代中 eden 和 S0/S1 空间的比例默认-XX:SurvivorRatio=8,Eden:S0:S1 =8:1:1
    • 假如 -XX:SurvivorRatio=4,Eden:S0:S1 =4:1:1
    • SurvivorRatio 值就是设置 eden区的比例占多少,SO/S1相同
  • -XX:NewRatio
    • 配置 年轻代与老年代 在堆结构的占比
      • 默认:-XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3;
      • -XX:NewRatio=4 新生代占1,老年代4,年轻代占整个堆的1/5;
      • NewRatio 值就是设置老年代的占比,剩下的1给新生代。
  • -XX:MaxTenuringThreshold 设置垃圾最大年龄,Survivor 复制默认为 15 次。0~15
    • 如果设置为 0 的话,则新生代代对象不经过 Survivor 区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。
  • -XX: +UseSerialGC 串行垃圾回收器
  • -XX: +UseParallelGC 并行垃圾回收器

微服务相关:java -server JVM各种参数 -jar jar包名字


七、OOM Error

@L%M@2`7EN)NTMDR8CC2]MF.png

StackOverflowError

1
2
3
4
5
6
7
8
9
public class StackOverflowError {
public static void main(String[] args) {
StackOverflow();
}

private static void StackOverflow() {
StackOverflow();
}
}

Java heap space

1
2
3
4
5
6
7
8
9
10
11
12
public class JavaHeapSpace {

/**
* -Xms10m -Xmx10m
*/
public static void main(String[] args) {

while (true) {
byte[] bytes = new byte[10 * 1024 * 1024];
}
}
}

GC overhead limit exceeded

  • GC 回收时间过长时会抛出 OutOfMemroyError。过长的定义是,超过98%的时间用来做 GC 并且回收了不到2%的堆内存,连续多次 GC 都只回收了不到 2% 的极端情况下才会抛出。
  • 假如不抛出GC overhead limit 错误会发生什么情况呢?那就是 GC 清理的内存很快会再次被填满,迫使GC 再次执行。这样就形成恶性循环,CPU使用率一直是100%, 而GC 却没有任何成果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;
import java.util.List;

/**
* @author Yu
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*/
public class GCOverheadLimit {
public static void main(String[] args) {

int i = 0;
List<String> list = new ArrayList<>();

try {
while (true) {
list.add(String.valueOf(++i).intern());
}
} catch (Throwable e) {
System.out.println("i:" + i);//i:145884
e.printStackTrace();
throw e;
}
}
}

Direct buffer memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.nio.ByteBuffer;

/**
* @author Yu
* -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*/
public class DirectBufferMemory {
public static void main(String[] args) throws InterruptedException {
System.out.println("配置的maxDirectMemory:" + (sun.misc.VM.maxDirectMemory() / (double) 1024 / 1024) + "MB");
Thread.sleep(3000);

//设置为 5m 要分配 6m
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}

Unable to create new native thread

  • 高并发请求服务器时,出现此异常。native thread与对应平台有关。

    • new Thread().start();//内部调用本地方法 start0();
      private native void start0();
      <!--20-->
      

Metaspace

元空间(Java8)与永久代Java7)之间最大的区别在于:

  • 永久代使用的 JVM 的堆内存, 但是 Java8 以后的元空间并不在虚拟机中而是使用本机物理内存。
  • 元空间主要存放:虚拟机加载类的信息,常量池,静态变量,即时编译后的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* @author Yu
* -XX:MetaspaceSize=50m -XX:MaxMetaspaceSize=50m
*/
public class Metaspace {

static class OOM {}

public static void main(final String[] args) {

int count = 0;
try {
while (true) {
count++;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, args);
}
});
enhancer.create();
}
} catch (Throwable e) {
System.out.println("count 次后发生异常: " + count);
e.printStackTrace();
}
}
}

八、Error和Exception

Error类 和 Exception类都是继承 Throwable类。

  • Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

  • Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。Exception又分为两类:

    • CheckedException:(编译时异常) 需要用 try—catch 显示的捕获,对于可恢复的异常使用CheckedException。

    • UnCheckedException(RuntimeException):(运行时异常)不需要捕获,对于程序错误(不可恢复)的异常使用 RuntimeException。

常见的RuntimeException异常

  • illegalArgumentException:此异常表明向方法传递了一个不合法或不正确的参数。
  • illegalStateException:在不合理或不正确时间内唤醒一方法时出现的异常信息。
  • NullpointerException:空指针异常。
  • IndexOutOfBoundsException:索引超出边界异常

常见的CheckedException异常

  • 我们在编写程序过程中 try—catch 捕获到的一场都是 CheckedException。
  • io包中的IOExecption及其子类,都是 CheckedException。

Error 和 Exception 就像是水池和水池里的水的区别

  • “水池”,就是代码正常运行的外部环境,如果水池崩溃(系统崩溃),或者池水溢出(内存溢出)等,这些都是跟水池外部环境有关。这些就是java中的 Error。

  • “水池里的水”,就是正常运行的代码,水污染了、有杂质了,浑浊了,这些影响水质的因素就是 Exception。

感谢阅读


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !